package cn.wolfcode.web.controller;

import cn.wolfcode.common.anno.RequestUser;
import cn.wolfcode.common.domain.UserInfo;
import cn.wolfcode.common.exception.BusinessException;
import cn.wolfcode.common.web.Result;
import cn.wolfcode.common.web.anno.RequireLogin;
import cn.wolfcode.domain.OrderInfo;
import cn.wolfcode.domain.SeckillProductVo;
import cn.wolfcode.mq.DefaultMessageSendCallback;
import cn.wolfcode.mq.MQConstant;
import cn.wolfcode.mq.OrderMessage;
import cn.wolfcode.redis.SeckillRedisKey;
import cn.wolfcode.service.IOrderInfoService;
import cn.wolfcode.service.ISeckillProductService;
import cn.wolfcode.common.utils.AssertUtil;
import cn.wolfcode.util.DateUtil;
import cn.wolfcode.web.msg.SeckillCodeMsg;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


@RestController
@RequestMapping("/order")
@Slf4j
public class OrderInfoController {
    //本地内存，注意：由于该map是在类上定义的，即成员变量，又该类是单例的,则该map只有一份，导致大量请求同时进来，即多线程访问同一份共享资源，会有线程安全问题。
    //因此要用HashTable或ConcurrentHashMap 线程安全的
    //Hashtable和ConcurrentHashMap的key和value均不能为空.Hashtable用synchronized锁put整个方法，而ConcurrentHashMap锁put的代码块,锁粒度小,因此性能高
    //HashMap的key和value均可为空
    private static final Map<Long, Boolean> STOCK_OVER_FLOW_MAP = new ConcurrentHashMap<>();
    private final ISeckillProductService seckillProductService;
    private final StringRedisTemplate redisTemplate;
    private final IOrderInfoService orderInfoService;
    private final RocketMQTemplate rocketMQTemplate;

    public OrderInfoController(ISeckillProductService seckillProductService, StringRedisTemplate redisTemplate, IOrderInfoService orderInfoService, RocketMQTemplate rocketMQTemplate) {
        this.seckillProductService = seckillProductService;
        this.redisTemplate = redisTemplate;
        this.orderInfoService = orderInfoService;
        this.rocketMQTemplate = rocketMQTemplate;
    }

    //这样写，表示调用方只具备删除的能力，而不具备该Map的所有能力，如果是成员变量public的话，可以被替换，因此不安全
    public static void removeStockOverFlagKey(Long seckillId){
        STOCK_OVER_FLOW_MAP.remove(seckillId);
    }

    /**
     * 立即抢购秒杀商品
     * 优化前：
     * QPS：90/s
     * <p>
     * 存在超卖问题
     * -JVM内置锁
     * -分布式锁
     * -MySQL乐观锁
     * <p>
     * 优化后：
     * QPS：1000/s
     */
    @RequireLogin
    @PostMapping("/doSeckill")
    public Result<String> doSeckill(Long seckillId, Integer time, @RequestUser UserInfo userInfo, @RequestHeader("token") String token) {

        //本地内存库存售完标记(JVM缓存库存售完标记)
        //判断库存是否已经卖完了，如果已经卖完，直接返回异常，后续步骤就不再走，因此可以提升吞吐率
        Boolean flag = STOCK_OVER_FLOW_MAP.get(seckillId);
        if (flag != null && flag) {
            return Result.error(SeckillCodeMsg.SECKILL_STOCK_OVER);
        }
        //秒杀商品抢购流程
        //1.判断用户是否登录 => 注解实现
        //2.基于秒杀 id + 场次查询秒杀商品对象
        SeckillProductVo sp = seckillProductService.selectByIdAndTime(seckillId, time);
        //断言是否有该秒杀商品
        if (sp == null) {
            return Result.error(SeckillCodeMsg.REMOTE_DATA_ERROR);
        }
        //3.判断秒杀的商品在当前时间是否在秒杀时间范围内
        Boolean range = DateUtil.betweenSeckillDateTime(sp.getStartDate(), time);
        if (!range) {
            return Result.error(SeckillCodeMsg.OUT_OF_SECKILL_TIME_ERROR);
        }
        //4.判断用户是否已经下过订单
        //OrderInfo orderInfo = orderInfoService.selectByUserIdAndSeckillId(userInfo.getPhone(), seckillId, time);
        //AssertUtil.isTrue(orderInfo == null, "不能重复下单");
        //判断用户是否重复下单，问题1：并发安全问题[如果没有下过订单，则走到判断库存，此时库存又不足，则需回滚，则删除用户下订单的标记] 问题2：通过数据库判断，数据库压力过大。
        //解决方案：问题2解决：由于Redis的高性能以及单线程可保证原子性操作。则通过Redis查询判断用户是否重复下单。
        String userOrderFlagKey = SeckillRedisKey.SECKILL_ORDER_HASH.join(seckillId + "");
        Long orderCount = redisTemplate.opsForHash().increment(userOrderFlagKey, userInfo.getPhone() + "", 1);
        if (orderCount > 1) {
            return Result.error(SeckillCodeMsg.REPEAT_SECKILL);
        }
        try {
            //5.判断库存是否充足
            //进行库存预减操作，当redis中的库存为0，则抛异常，则其他请求则不会进入到扣库存操作中。减少请求去操作数据库扣库存。
            //如果库存只有10，则只有10个请求进入到扣库存
            String hashKey = SeckillRedisKey.SECKILL_STOCK_COUNT_HASH.join(time);
            //返回扣完库存后剩余的数量
            Long stockCount = redisTemplate.opsForHash().increment(hashKey, seckillId + "", -1);
            //如果剩余的库存数量>=0，则往下执行，否则抛异常
            AssertUtil.isTrue(stockCount >= 0, SeckillCodeMsg.SECKILL_STOCK_OVER.getMsg()); //此处会抛异常，回滚操作，则需要再判断用户是否已经下过订单标记删除
            //6.扣减库存，创建订单对象，返回订单id
            // orderNo = orderInfoService.doSeckill(userInfo.getPhone(), sp); //当库存不足，扣除库存失败，此处会抛异常，回滚操作，则需要再判断用户是否已经下过订单标记删除
            // 优化方案：通过RockctMQ异步发送消息，实现异步请求，再通过监听MQ消息，进行订单处理
            rocketMQTemplate.asyncSend(
                    MQConstant.ORDER_PENDING_TOPIC,
                    new OrderMessage(time,seckillId,token,userInfo.getPhone()),
                    new DefaultMessageSendCallback("创建订单"));
        } catch (BusinessException e) {
            //当库存不足，直接标记库存售完
            STOCK_OVER_FLOW_MAP.put(seckillId, true);
            //把用户下过订单的标记删除，即回滚操作
            redisTemplate.opsForHash().delete(userOrderFlagKey, userInfo.getPhone() + "");
            return Result.error(e.getCodeMsg());
        }
        return Result.success("创建订单中,请稍等!!!");
    }

    /**
     * 根据订单编号查询订单，并且限制该订单只能自己查询
     * @param orderNo 订单编号
     * @param userInfo 登录的用户信息
     * @return 订单结果
     */
    @RequireLogin
    @GetMapping("/find")
    public Result<OrderInfo> find(String orderNo,@RequestUser UserInfo userInfo){
       OrderInfo orderInfo = orderInfoService.findByOrderNo(orderNo);
       //判断该订单是否是该用户的订单，如果不是则违规操作
       if(!userInfo.getPhone().equals(orderInfo.getUserId())){
           return Result.error(SeckillCodeMsg.REMOTE_DATA_ERROR);
       }
       return Result.success(orderInfo);
    }
}
